#!/usr/bin/env ruby
#
# Created by Jerry Luk on 2008-04-30
# Copyright (c) 2008. All rights reserved.

#  GDataService provides CRUD operations, and programmatic login for GData services
begin
  require 'system_timer'
  GDataTimer = SystemTimer
rescue LoadError
  require 'timeout'
  GDataTimer = Timeout
end

module Google
  class GDataService
    include Google::UrlUtils
    
    PROGRAMMATIC_AUTH_LABEL = 'GoogleLogin auth'
    AUTHSUB_AUTH_LABEL = 'AuthSub token='
    CLIENT_LOGIN_LABEL = 'GoogleLogin auth='
    AUTH_SERVER_HOST = 'https://www.google.com'
    
    attr_accessor :service
    attr_accessor :server
    attr_accessor :email
    attr_accessor :password
    attr_accessor :additional_headers
    attr_accessor :auth_token
    attr_accessor :timeout_duration
    attr_accessor :gsession_id
    attr_accessor :source # The source is the name of the application making the request. It should be in the form company_id-app_name-app_version
    
    def initialize()
      @server = nil
      @scope = nil
      @auth_token = nil
      @has_session_token = false
      @use_client_login = false
      @gsession_id = nil
      @timeout_duration = 60
      @source = 'jerryluk.com-google_fu-1.0'
      self.additional_headers ||= []
    end    
    
    # Generates a URL at which the user will login and be redirected back.
    #
    # ==== Options
    # +next_url+:: The URL user will be sent to after logging in..
    # +scope+:: The URL of the service to be accessed. Usually the subclass will set 
    #           this for you
    # +secure+:: Optional. Determines whether or not the issued token is a secure token
    # +session+:: Optional. Determines whether or not the issued token can be upgraded to a session token.
    # 
    def generate_auth_sub_url(next_url, scope=nil, secure=false, session=true)
      @scope = scope if scope
      request_params = form_encoded({:next => next_url, 
                                     :scope => @scope,
                                     :secure => (secure ? 1 : 0),
                                     :session => (session ? 1 : 0)})
      "#{AUTH_SERVER_HOST}/accounts/AuthSubRequest?#{request_params}"
    end
    
    # Upgrades a single use AuthSub token to a long-lived session token
    def upgrade_auth_token
      raise "No AuthSub token is existed" unless @auth_token
      raise "AuthToken is already a session token" if @has_session_token
      
      response_body = nil
      begin
        GDataTimer.timeout(timeout_duration) do
          response = open("#{AUTH_SERVER_HOST}/accounts/AuthSubSessionToken", 
                          :method => :get,
                          'Authorization' => auth_header,
                          'Content-Type' => 'application/x-www-form-urlencoded')
          response_body = response.read unless response.nil?        
        end
      end
      @auth_token = response_body.gsub(/^Token=/, '')
      @has_session_token = true
    end
    
    # Revokes an existing AuthSub token
    def revoke_auth_token
      raise "No AuthSub token is existed" unless @auth_token
      begin
        GDataTimer.timeout(timeout_duration) do
          response = open("#{AUTH_SERVER_HOST}/accounts/AuthSubRevokeToken", 
                          :method => :get,
                          'Authorization' => auth_header,
                          'Content-Type' => 'application/x-www-form-urlencoded')       
        end
      end
      @auth_token = nil
      @has_session_token = false
    end
    
    def session_token
      @has_session_token ? @auth_token : nil
    end
    
    def session_token=(token)
      @auth_token = token
      @has_session_token = true
    end
    
    # Authenticates the user and sets the GData Auth token.
    # 
    # Login retreives a temporary auth token which must be used with all
    # requests to GData services. The auth token is stored in the GData client
    # object
    # 
    # ==== Options
    # +username+:: The username of the account
    # +password+:: The password of the account
    # +service+:: Optional. The service you want to use
    # +source+:: Optional. The URL for your service
    # +account_type+:: Optional. The default is 'HOSTED_OR_GOOGLE'
    #
    def client_login(username, password, service=nil, source=nil, account_type=nil)
      @email = username
      @password = password
      @service ||= service
      @source ||= source
      
      response_body = nil
      form = form_encoded({'Email' => @email,
                           'Passwd' => @password,
                           'accountType' => 'HOSTED_OR_GOOGLE',
                           'service' => @service,
                           'source' => @source})
      begin
        GDataTimer.timeout(timeout_duration) do
          response = open("#{AUTH_SERVER_HOST}/accounts/ClientLogin", 
                          :method => :post,
                          :body => form,
                          'Content-Length' => form.length.to_s,
                          'Content-Type' => 'application/x-www-form-urlencoded')
          response_body = response.read unless response.nil?        
        end
      end
      @use_client_login = true
      @auth_token = response_body.split(/=/).last
    end
    
    ### CRUD operations
    
    # Queries the GData API with the given URI and return a GData
    # 
    # The uri is the portion of the URI after the server value 
    # (ex: www.google.com).
    # 
    # To perform a query against Google Base, set the server to 
    # 'base.google.com' and set the uri to '/base/feeds/...', where ... is 
    # your query. For example, to find snippets for all digital cameras uri 
    # should be set to: '/base/feeds/snippets?bq=digital+camera'
    # 
    # ==== Options
    # +uri+:: The query in the form of a URI. (e.g., )    <tt>'/base/feeds/snippets?bq=digital+camera'</tt>
    # +headers+:: Extra HTTP headers to be included
    #          in the GET request. These headers are in addition to 
    #          those stored in the client's additional_headers property.
    #          The client automatically sets the Content-Type and 
    #          Authorization headers.
    # +params+:: Additional URL parameters to be included
    #         in the URI. These are translated into query arguments
    #         in the form '&dict_key=value&...'.
    #         (e.g., ) <tt>{'max-results': '250'}</tt> would yield a
    #         param <tt>&max-results=250</tt>
    # 
    def get(uri, params={}, headers={})
      request(uri, :get, headers, params)
    end
    
    # Inserts data into a GData service at the given URI and return a
    # GData object
    # 
    # ==== Options
    # +data+:: The XML or GData object to be sent to the uri
    # +uri+:: The feed URI where the data should be inserted
    # +headers+:: Extra HTTP headers to be included
    #          in the GET request. These headers are in addition to 
    #          those stored in the client's additional_headers property.
    #          The client automatically sets the Content-Type and 
    #          Authorization headers.
    # +params+:: Additional URL parameters to be included
    #         in the URI. These are translated into query arguments
    #         in the form '&dict_key=value&...'.
    #         (e.g., ) <tt>{'max-results': '250'}</tt> would yield a
    #         param <tt>&max-results=250</tt>
    #
    def post(data, uri, params={}, headers={})
      data = data.atom.to_xml if data.kind_of? GData
      request(uri, :post, headers, params, data)
    end
    
    # Updates an entry at the given URI and return a GData object
    # 
    # ==== Options
    # +data+:: The XML or GData object to be sent to the uri
    # +uri+:: The URI of the entry
    # +headers+:: Extra HTTP headers to be included
    #          in the GET request. These headers are in addition to 
    #          those stored in the client's additional_headers property.
    #          The client automatically sets the Content-Type and 
    #          Authorization headers.
    # +params+:: Additional URL parameters to be included
    #         in the URI. These are translated into query arguments
    #         in the form '&dict_key=value&...'.
    #         (e.g., ) <tt>{'max-results': '250'}</tt> would yield a
    #         param <tt>&max-results=250</tt>
    #
    def put(data, uri, params={}, headers={})
      data = data.atom.to_xml if data.kind_of? GData
      request(uri, :put, headers, params, data)
    end
    
    # Deletes the entry at the given URI. Return true if success
    # 
    # ==== Options
    # +data+:: The XML or GData object to be sent to the uri
    # +uri+:: The feed URI to which the data should be deleted
    # +headers+:: Extra HTTP headers to be included
    #          in the GET request. These headers are in addition to 
    #          those stored in the client's additional_headers property.
    #          The client automatically sets the Content-Type and 
    #          Authorization headers.
    # +params+:: Additional URL parameters to be included
    #         in the URI. These are translated into query arguments
    #         in the form '&dict_key=value&...'.
    #         (e.g., ) <tt>{'max-results': '250'}</tt> would yield a
    #         param <tt>&max-results=250</tt>
    #
    def delete(uri, params={}, headers={})
      result = request(uri, :delete, headers, params)
      !!result
    end    
    
    private
    def auth_header
      @use_client_login ? CLIENT_LOGIN_LABEL + @auth_token : AUTHSUB_AUTH_LABEL + @auth_token
    end
    
    def request(uri, method=:get, headers={}, params={}, body=nil)
      headers['Content-Type'] ||= 'application/atom+xml'
      headers['Authorization'] ||= auth_header if @auth_token
      headers['Content-Length'] = body.length.to_s if body
      
      raw_data = params.delete(:raw_data)
      
      uri = "#{@server}#{uri}" if uri[0].chr == '/' and @server
      if @gsession_id and !(uri.include? 'gsessionid=') and params[:gsessionid].nil?
        params[:gsessionid] = @gsession_id
      end
      
      unless params.empty?
        uri += (uri.include? '?') ? '&' + form_encoded(params) : 
                                    '?' + form_encoded(params)
      end
      
      data = nil
      begin
        GDataTimer.timeout(timeout_duration) do
          response = nil
          if method == :post or method == :put
            response = open(uri, headers.merge({ :method => method, :body => body }))
          else
            response = open(uri, headers.merge({ :method => method }))
          end
          if raw_data
            data = response.read
          else
            atom = RSS::Parser.parse(response, false)
            data = GData.new(atom)
          end
          # response_body = http_response.read unless response.nil?        
        end
      end
      data
    end
  end
end